package consensus

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/caivega/glog"
	libaccount "github.com/tokentransfer/interfaces/account"
	libblock "github.com/tokentransfer/interfaces/block"
	libcore "github.com/tokentransfer/interfaces/core"
	libstore "github.com/tokentransfer/interfaces/store"
	"github.com/tokentransfer/node/block"
	"github.com/tokentransfer/node/core"
	"github.com/tokentransfer/node/core/pb"
	"github.com/tokentransfer/node/storage"
	"github.com/tokentransfer/node/util"
)

type Status int

const (
	PeerNone Status = iota
	PeerConnected
	PeerKnown
	PeerSynchronized
	PeerConsensused
)

func (s Status) String() string {
	switch s {
	case PeerNone:
		return "none"
	case PeerConnected:
		return "connected"
	case PeerKnown:
		return "known"
	case PeerSynchronized:
		return "synchronized"
	case PeerConsensused:
		return "consensused"
	}
	return "unknown status"
}

type peerEntry struct {
	root   libcore.Address
	status Status

	BlockNumber int64
	PeerCount   int64
	PeerStatus  Status

	running   bool
	infoCount int64
	lastTime  int64

	lastSendBlock    int64
	lastSendTime     time.Time
	lastRequestBlock int64
	lastRequestTime  time.Time
}

type Peer struct {
	Id  string
	Key libaccount.PublicKey

	running bool
	ctx     context.Context
	cancel  context.CancelFunc

	ins  chan []byte // the content data in message
	outs chan []byte // the message data

	dataChan chan []byte // the message data for data channel

	address libcore.Address
	index   uint64

	peerMap    *sync.Map
	peermap    map[string]*peerEntry
	peerLocker *sync.RWMutex
}

func (n *Node) getPeerEntry(p *Peer, rootAccount libcore.Address) *peerEntry {
	root := util.GetString(rootAccount)
	e, ok := p.peerMap.Load(root)
	if ok {
		return e.(*peerEntry)
	}

	p.peerLocker.Lock()
	defer p.peerLocker.Unlock()

	e, ok = p.peerMap.Load(root)
	if ok {
		return e.(*peerEntry)
	}

	e = &peerEntry{
		root:   rootAccount,
		status: PeerConnected,

		BlockNumber: int64(-1),
		PeerCount:   int64(0),
		PeerStatus:  PeerConnected,

		running:          true,
		lastSendBlock:    int64(-1),
		lastSendTime:     time.Now(),
		lastRequestBlock: int64(-1),
		lastRequestTime:  time.Now(),
	}
	go n.sync(p, e.(*peerEntry))
	p.peerMap.Store(root, e)
	p.peermap[root] = e.(*peerEntry)
	return e.(*peerEntry)
}

func (n *Node) sync(p *Peer, e *peerEntry) {
	if n.self != p {
		n.SendPeerInfo(e.root, p)
	}
	for {
		if !p.running || !e.running || n.self == p {
			return
		}
		rootAccount := e.root
		entry, err := n.GetEntry(rootAccount)
		if err != nil {
			glog.Error(err)
			return
		} else {
			util.Sleep(
				200*time.Millisecond,
				time.Duration(n.config.GetBlockDuration())*time.Second,
				n._blockNumberChanged,
				n._peerStatusChanged,
				e, entry, p,
			)
		}
	}
}

func (n *Node) _peerStatusChanged(o []interface{}) {
	e := o[0].(*peerEntry)
	entry := o[1].(*Entry)
	p := o[2].(*Peer)

	s := func(e *peerEntry) Status {
		if e.status > e.PeerStatus {
			return e.PeerStatus
		}
		return e.status
	}(e)
	switch s {
	case PeerNone:
		n.ConnectTo(entry.root, p)
	case PeerConnected:
		n.SendPeerInfo(entry.root, p)
	case PeerKnown:
		entry.Synchronized = n.PrepareSynchronized(entry.root)
	case PeerSynchronized:
		entry.Consensused = n.PrepareConsensused(entry.root)
	case PeerConsensused:
		n.SendPeerInfo(entry.root, p)
	}
}

func (n *Node) _blockNumberChanged(o []interface{}) bool {
	e := o[0].(*peerEntry)
	entry := o[1].(*Entry)
	p := o[2].(*Peer)
	if e.status >= PeerKnown && e.PeerStatus >= PeerKnown {
		if (entry.GetBlockNumber() >= 0 && entry.GetBlockNumber() > e.BlockNumber) && ((e.lastSendBlock < 0) || (e.lastSendBlock >= 0 && entry.GetBlockNumber() > e.lastSendBlock)) {
			if e.lastSendBlock < 0 || e.BlockNumber == e.lastSendBlock || time.Since(e.lastSendTime) > (5*time.Second) {
				if e.running && p.running && time.Since(time.Unix(e.lastTime/1e9, e.lastTime%1e9)) > (3*time.Second) {
					select {
					case <-p.ctx.Done():
						return false
					default:
						blockNumber := int64(e.BlockNumber + 1)
						e.lastSendBlock = blockNumber
						e.lastSendTime = time.Now()
						err := n.sendBlock(entry, p, uint64(blockNumber))
						if err != nil {
							glog.Error(err)
						}
					}
				}
				return true
			} else {
				if e.lastSendBlock >= 0 && entry.GetBlockNumber() <= e.lastSendBlock {
					e.lastSendBlock = -1
				}
			}
		} else {
			if (entry.GetBlockNumber() < e.BlockNumber) && ((e.lastRequestBlock < 0) || (e.lastRequestBlock >= 0 && e.lastRequestBlock > entry.GetBlockNumber())) {
				_, ok := entry.replyMap.Load(fmt.Sprintf("%d", e.lastRequestBlock))
				if e.lastRequestBlock < 0 || (ok || e.lastRequestBlock <= entry.GetBlockNumber()) || time.Since(e.lastRequestTime) > (5*time.Second) {
					if e.running && p.running {
						inCachedCount := 0
						blockNumber := entry.GetBlockNumber() + 1
						for {
							if blockNumber <= e.BlockNumber {
								t, ok := entry.requestMap.Load(fmt.Sprintf("%d", blockNumber))
								if ok {
									_, inCached := entry.replyMap.Load(fmt.Sprintf("%d", blockNumber))
									if inCached {
										inCachedCount++
									}
									if inCachedCount > 3 {
										return false
									}
									if time.Since(t.(time.Time)) > (5*time.Second) && !inCached && blockNumber > entry.GetBlockNumber() {
										break
									} else {
										blockNumber++
									}
								} else {
									entry.requestMap.Store(fmt.Sprintf("%d", blockNumber), time.Now())
									break
								}
							} else {
								break
							}
						}
						rootData, err := block.AddressToByte(e.root)
						if err != nil {
							glog.Error(err)
						} else {
							requestData, err := core.Marshal(&pb.RequestInfo{
								Account:     rootData,
								BlockNumber: blockNumber,
							})
							if err != nil {
								glog.Error(err)
							} else {
								msgData, err := n.EncodeMessage(requestData)
								if err != nil {
									glog.Error(err)
								} else {
									if e.running && p.running {
										e.lastRequestBlock = blockNumber
										e.lastRequestTime = time.Now()
										select {
										case <-p.ctx.Done():
											return false
										default:
											p.dataChan <- msgData
											glog.Infof(">>> request block @%s: %d, %d -> %v, %d", util.GetString(entry.root), entry.GetBlockNumber(), blockNumber, p.GetAddress(n), inCachedCount)
											// glog.Infoln("request map", util.List(entry.requestMap))
											// glog.Infoln("reply map", util.List(entry.replyMap))
										}
									}
								}
							}
						}
					}
					return true
				}
			} else {
				if e.lastSendBlock >= 0 && entry.GetBlockNumber() <= e.lastSendBlock {
					e.lastSendBlock = -1
				}
				if e.lastRequestBlock >= 0 && e.lastRequestBlock <= entry.GetBlockNumber() {
					e.lastRequestBlock = -1
				}
			}
		}
	}
	return false
}

func (p *Peer) GetPublicKey() libaccount.PublicKey {
	return p.Key
}

func (p *Peer) GetIndex(n *Node) uint64 {
	if p.index == 0 && p.Key != nil {
		address, err := p.Key.GenerateAddress()
		if err != nil {
			return 0
		}
		index, err := n.GetIndex(address.String())
		if err != nil {
			return 0
		}

		p.address = address
		p.index = index
	}
	return p.index
}

func (p *Peer) GetAddress(n *Node) libcore.Address {
	if p.address == nil && p.Key != nil {
		address, err := p.Key.GenerateAddress()
		if err != nil {
			return nil
		}
		index, err := n.GetIndex(address.String())
		if err != nil {
			return nil
		}

		p.address = address
		p.index = index
	}
	return p.address
}

type Entry struct {
	Synchronized bool
	Consensused  bool

	ValidatedBlock libblock.Block

	root    libcore.Address
	merkle  libstore.MerkleService
	storage *storage.StorageService

	serviceMap *sync.Map

	requestMap  *sync.Map
	requestChan chan *requestEntry

	replyMap  *sync.Map
	replyChan chan *block.Block

	txlist []libblock.TransactionWithData
	txmap  map[string]libblock.TransactionWithData
}

func (entry *Entry) GetMerkleService() libstore.MerkleService {
	return entry.merkle
}

func (entry *Entry) GetStorageService() *storage.StorageService {
	return entry.storage
}

func (entry *Entry) GetBlockNumber() int64 {
	if entry.ValidatedBlock != nil {
		return int64(entry.ValidatedBlock.GetIndex())
	}
	return -1
}

func (entry *Entry) GetBlockHash() string {
	if entry.ValidatedBlock != nil {
		return entry.ValidatedBlock.GetHash().String()
	}
	return ""
}

func (entry *Entry) GetBlock() libblock.Block {
	if entry.ValidatedBlock != nil {
		return entry.ValidatedBlock
	}
	return nil
}

func (entry *Entry) GetAccountInfo(account libcore.Address) (*block.AccountState, error) {
	ms := entry.merkle

	accountKey := account.String()
	state, err := ms.GetStateByTypeAndKey(block.ACCOUNT_STATE, accountKey)
	if err != nil {
		return nil, err
	}
	info, ok := state.(*block.AccountState)
	if !ok {
		return nil, util.ErrorOfInvalid("data", "account state")
	}
	return info, nil
}

func (n *Node) GetEntry(rootAccount libcore.Address) (*Entry, error) {
	root := util.GetString(rootAccount)
	entry, ok := n.entryMap.Load(root)
	if ok {
		return entry.(*Entry), nil
	}

	n.entryLocker.Lock()
	defer n.entryLocker.Unlock()

	entry, ok = n.entryMap.Load(root)
	if ok {
		return entry.(*Entry), nil
	}

	ms, err := n.getMerkleService(rootAccount)
	if err != nil {
		return nil, err
	}
	ss, err := n.getStorageService(rootAccount)
	if err != nil {
		return nil, err
	}
	entry = &Entry{
		root:    rootAccount,
		merkle:  ms,
		storage: ss,

		serviceMap: &sync.Map{},

		requestMap:  &sync.Map{},
		requestChan: make(chan *requestEntry, 32),

		replyMap:  &sync.Map{},
		replyChan: make(chan *block.Block, 32),

		txlist: make([]libblock.TransactionWithData, 0),
		txmap:  make(map[string]libblock.TransactionWithData),
	}
	err = n.load(rootAccount, entry.(*Entry))
	if err != nil {
		_ = ms.Close()
		return nil, err
	}
	n.entryMap.Store(root, entry)
	n.entrymap[root] = entry.(*Entry)
	go n.checkServices(entry.(*Entry))
	go n.requestBlocks(entry.(*Entry))
	go n.replyBlocks(entry.(*Entry))
	return entry.(*Entry), nil
}

type requestEntry struct {
	p *Peer
	r *pb.RequestInfo
	t int64
}
